
/**
 * AppleIIGo
 * SDL I/O class
 * (C) 2007-2008 by Marc S. Ressl (mressl@umich.edu)
 * Released under the GPL
 */

#include "SDLIO.h"
#include "KeyDownList.h"

int runSdlThread(void * userdata) {
	((SDLIO *) userdata)->runThread();
	return 0;
}

void callbackSdl(void * userdata, Uint8 * stream, int len) {
	((SDLIO *) userdata)->callbackAudio((SINT16 * ) stream, len >> 2);
}

UINT32 keypadSym[] = {
	SDLK_KP0, SDLK_KP1, SDLK_KP2, SDLK_KP3, SDLK_KP4,
	SDLK_KP5, SDLK_KP6, SDLK_KP7, SDLK_KP8, SDLK_KP9,
	SDLK_KP_PERIOD, SDLK_KP_DIVIDE, SDLK_KP_MULTIPLY,
	SDLK_KP_MINUS, SDLK_KP_PLUS, SDLK_KP_ENTER, SDLK_KP_EQUALS,
};

SDLIO::SDLIO() {
	initThread();
	initAudio();
	initVideo();
	initKeyboard();
	initJoystick();
	initMouse();
}

SDLIO::~SDLIO() {
}

bool SDLIO::open(Motherboard * motherboard, char * caption) {
	this->motherboard = motherboard;
	
	if (SDL_Init(SDL_INIT_AUDIO | SDL_INIT_VIDEO | SDL_INIT_JOYSTICK) < 0) {
        fprintf(stderr, "Unable to initialize SDL: %s\n", SDL_GetError());
		return false;
    }
	
	openJoystick();
	openMouse();
	openKeyboard();
	
	if (!openVideo(caption)) {
		fprintf(stderr, "Unable to open SDL video: %s\n", SDL_GetError());
		return false;
	}
	
	if (!openAudio()) {
		fprintf(stderr, "Unable to open SDL audio: %s\n", SDL_GetError());
		return false;
	}
	
	if (!openThread()) {
		fprintf(stderr, "Unable to open SDL thread: %s\n", SDL_GetError());
		return false;
	}
	
    SDL_PauseAudio(0);
	
	return true;
}

bool SDLIO::reopen() {
	SDL_UserEvent event;
	
	event.type = SDL_USEREVENT;
	event.code = 0;
	return (SDL_PushEvent(&event) != -1);
}

void SDLIO::close() {
	SDL_PauseAudio(1);
	
	closeThread();
	closeAudio();
	closeVideo();
	closeKeyboard();
	closeMouse();
	closeJoystick();
	
	SDL_Quit();
}

void SDLIO::lock() {
	if (!mutex)
		return;
	
	if (SDL_mutexP(mutex) == -1) {
		fprintf(stderr, "Unable to lock SDL mutex: %s\n", SDL_GetError());
		exit(-1);
	}
}

void SDLIO::unlock() {
	if (!mutex)
		return;
	
	if (SDL_mutexV(mutex) == -1) {
		fprintf(stderr, "Unable to unlock SDL mutex: %s\n", SDL_GetError());
		exit(-1);
	}
}

void SDLIO::initThread() {
	mutex = NULL;
	cond = NULL;
	thread = NULL;
	
	isThreadPausedValue = false;
}

bool SDLIO::openThread() {
	bool isError = false;
	
	if (!isError)
		isError = (mutex = SDL_CreateMutex()) == NULL;
	if (!isError)
		isError = (cond = SDL_CreateCond()) == NULL;	
	if (!isError) {
		isKillThreadSignal = false;
		isError = (thread = SDL_CreateThread(runSdlThread, this)) == NULL;
	}
	
	if (isError) {
		closeThread();
		return false;
	}
	
	return true;
}

void SDLIO::closeThread() {
	if (thread) {
		lock();
		isKillThreadSignal = true;
		SDL_CondSignal(cond);
		unlock();
		SDL_WaitThread(thread, NULL);
	}
	thread = NULL;
	
	if (cond)
		SDL_DestroyCond(cond);
	cond = NULL;
	if (mutex)
		SDL_DestroyMutex(mutex);
	mutex = NULL;
}

void SDLIO::setThreadPaused(bool value) {
	isThreadPausedValue = value;
}

bool SDLIO::isThreadPaused() {
	return isThreadPausedValue;
}

void SDLIO::runThread() {
	lock();
	while (!isKillThreadSignal) {
		if (SDL_CondWait(cond, mutex) == -1) {
			fprintf(stderr, "Unable to wait for SDL condition: %s\n",
					SDL_GetError());
			exit(-1);
		}
		
		// To-Do: Add NTSC/PAL change
		/*
		if (isPalTiming != motherboard->getTimingGenerator()->isPalTiming()) {
			closeAudio();
			openAudio();
		}*/
		
		if (SDL_MUSTLOCK(videoSurface)) 
			if (SDL_LockSurface(videoSurface) < 0) {
				fprintf(stderr, "Unable to lock SDL surface: %s\n",
						SDL_GetError());
				exit(-1);
			}
		
		motherboard->getVideoRenderer()->setFramebuffer((UINT32 *)
														videoSurface->pixels,
														videoSurface->pitch);
		if (!isThreadPaused()) {
			// To-Do: When sampleBuffer is larger than number of samples
			// in a frame
			// call multiple times, move the extra part to the beginning
			motherboard->processFrame();
		}
		
		if (SDL_MUSTLOCK(videoSurface))
			SDL_UnlockSurface(videoSurface);
		
		if (motherboard->getVideoRenderer()->isFramebufferUpdated()) {
			motherboard->getVideoRenderer()->setIsFramebufferUpdated(false);
			SDL_Flip(videoSurface);
		}
	}
	unlock();
}

void SDLIO::initAudio() {
	currentAudioVolume = audioVolume = 1.0;
}

bool SDLIO::openAudio() {
	SDL_AudioSpec audioFormat;
	bool isError = false;
	
	audioInputBuffer = new SINT32[SDLIO_AUDIO_SAMPLENUM];
	audioOutputBuffer = new SINT32[SDLIO_AUDIO_SAMPLENUM];
	
	audioFrameSampleNum = (SDLIO_AUDIO_SAMPLERATE /
						   motherboard->getTimingGenerator()->getFrameRate());
	SINT16 cpuByteOrderTestValue = 1;
	
	audioFormat.freq = SDLIO_AUDIO_SAMPLERATE;
	audioFormat.format = (*((char *) &cpuByteOrderTestValue) == 1) ?
		AUDIO_S16LSB : AUDIO_S16MSB;
	audioFormat.channels = 2;
	audioFormat.samples = audioFrameSampleNum;
	audioFormat.callback = callbackSdl;
	audioFormat.userdata = this;
	
	isError = (SDL_OpenAudio(&audioFormat, NULL) != 0);
	
	if (isError) {
		closeAudio();
		return false;
	}
	
	motherboard->setAudioConfiguration(audioInputBuffer,
									   audioOutputBuffer,
									   audioFrameSampleNum);
	
	return true;
}

void SDLIO::closeAudio() {
	SDL_CloseAudio();
	
	delete audioInputBuffer;
	delete audioOutputBuffer;
}

void SDLIO::setAudioVolume(float value) {
	audioVolume = value;
}

void SDLIO::callbackAudio(SINT16 * sampleData, UINT32 audioBufferSampleNum) {
	lock();
	
	if (audioBufferSampleNum != audioFrameSampleNum)
		printf("ERR: audioBufferSampleNum<>audioFrameSampleNum (%d vs. %d)\n", 
			   audioBufferSampleNum, audioFrameSampleNum);
	
	SINT32 * inputBuffer = audioInputBuffer;
	SINT32 * outputBuffer = audioOutputBuffer;
	for (UINT32 index = 0; index < audioBufferSampleNum; index++) {
		if (currentAudioVolume != audioVolume)
			currentAudioVolume = currentAudioVolume *
				(1.0F - SDLIO_AUDIO_VOLUMEFILTERFACTOR)
				+ audioVolume * SDLIO_AUDIO_VOLUMEFILTERFACTOR;
		
		*inputBuffer++ = *sampleData;
		*sampleData++ = saturateAudio((SINT32) (*outputBuffer *
												currentAudioVolume));
		*outputBuffer++ = 0;
		
		*inputBuffer++ = *sampleData;
		*sampleData++ = saturateAudio((SINT32) (*outputBuffer *
												currentAudioVolume));
		*outputBuffer++ = 0;
	}
	unlock();
	SDL_CondSignal(cond);
}

void SDLIO::initVideo() {
	isFullscreenValue = false;
	videoSurface = NULL;
	videoCaption = "";
}

bool SDLIO::openVideo(char * caption) {
	bool isError = false;
	
	if (!isError)
		isError = (videoSurface =
				   SDL_SetVideoMode(SDLIO_VIDEO_X, SDLIO_VIDEO_Y, 32,
									SDL_HWSURFACE |
									(isVideoFullscreen() ? SDL_FULLSCREEN : 0))
		) == NULL;
	
	if (!isError) {
		if (caption)
			videoCaption = caption;
		SDL_WM_SetCaption(videoCaption, NULL);
	}
	
	if (isError)
		closeVideo();
	
	return !isError;
}

void SDLIO::closeVideo() {
}

void SDLIO::setVideoFullscreen(bool value) {
	isFullscreenValue = value;
	reopen();
}

void SDLIO::toggleVideoFullscreen() {
	isFullscreenValue = !isFullscreenValue;
	reopen();
}

bool SDLIO::isVideoFullscreen() {
	return isFullscreenValue;
}

void SDLIO::initKeyboard() {
	keyDownList = NULL;
	
	resetCount = 0;
}

void SDLIO::openKeyboard() {
	SDL_EnableKeyRepeat(500, 100);
	SDL_EnableUNICODE(1);
	
	keyDownList = new KeyDownList();
}

void SDLIO::closeKeyboard() {
	for (keyDownList->setStart();
		 keyDownList->isNext();
		 keyDownList->setStart())
		pullKeyboardKey(keyDownList->getKey(), 0, 0);
	
	delete keyDownList;
}

void SDLIO::pushKeyboardKey(UINT32 sym, UINT32 mod, UINT32 unicode) {
	lock();
	
	if (!isVideoFullscreen() &&
		((((sym == SDLK_LCTRL) || (sym == SDLK_RCTRL)) && (mod & KMOD_ALT)) ||
		 (((sym == SDLK_LALT) || (sym == SDLK_RALT)) && (mod & KMOD_CTRL))))
		releaseMouse();
	
	switch (sym) {
		case SDLK_SPACE:
			if (mod & KMOD_CTRL)
				setThreadPaused(!isThreadPaused());
			break;
		case SDLK_BACKSPACE:
			if (mod & KMOD_CTRL) {
				if (mod & KMOD_META) {
					if (mod & KMOD_ALT)
						quit();
					else {
						if (!resetCount) {
							motherboard->getCpu()->assertReset(true);
							resetCount++;
						}
					}
				} else {
					if (!resetCount) {
						motherboard->getCpu()->assertReset(false);
						resetCount++;
					}
				}
				unicode = 0;
			} else
				unicode = 8;
			break;
		case SDLK_RETURN:
			if (mod & (KMOD_META | KMOD_ALT) && !(mod & KMOD_SHIFT)) {
				toggleVideoFullscreen();
				unicode = 0;
			}
			break;
		case SDLK_LEFT:
			unicode = 8;
			break;
		case SDLK_RIGHT:
			unicode = 21;
			break;
		case SDLK_UP:
			unicode = 11;
			break;
		case SDLK_DOWN:
			unicode = 10;
			break;
		case SDLK_DELETE:
			unicode = 127;
			break;
		case SDLK_F4:
			motherboard->getKeyboard()->toggleIIc40Switch();
			break;
		case SDLK_F7:
			motherboard->getCpu()->triggerNmi();
			break;
	}
	
	if (unicode > 0)
		motherboard->getKeyboard()->pushKey(unicode);
	
	keyDownList->pushKey(sym);
	motherboard->getKeyboard()->pushKeyModifier(getKeyboardModifierIndex(sym));
	motherboard->getKeyboard()->setAnyKeyDown(keyDownList->isAnyKeyDown());
	
	unlock();
}

void SDLIO::pullKeyboardKey(UINT32 sym, UINT32 mod, UINT32 unicode) {
	lock();
	
	if (sym == SDLK_BACKSPACE) {
		if (resetCount) {
			motherboard->getCpu()->clearReset();
			resetCount--;
		}
	}
	
	keyDownList->pullKey(sym);
	motherboard->getKeyboard()->pullKeyModifier(getKeyboardModifierIndex(sym));
	motherboard->getKeyboard()->setAnyKeyDown(keyDownList->isAnyKeyDown());
	
	unlock();
}

UINT32 SDLIO::getKeyboardModifierIndex(UINT32 sym) {
	if ((sym == SDLK_LSHIFT) || (sym == SDLK_RSHIFT))
		return KEYMODIFIER_SHIFT;
	if ((sym == SDLK_LCTRL) || (sym == SDLK_RCTRL))
		return KEYMODIFIER_CONTROL;
	if (sym == SDLK_CAPSLOCK)
		return KEYMODIFIER_CAPSLOCK;
	if ((sym == SDLK_LALT) || (sym == SDLK_RALT))
		return KEYMODIFIER_OPTION;
	if ((sym == SDLK_LMETA) || (sym == SDLK_RMETA))
		return KEYMODIFIER_COMMAND;
	
	for (int symIndex = 0; symIndex < sizeof(keypadSym) / sizeof(UINT32);
		 symIndex++) {
		if (sym == keypadSym[symIndex])
			return KEYMODIFIER_KEYPAD;
	}
	
	return KEYMODIFIER_END;
}

void SDLIO::initMouse() {
	for (UINT32 index = 0; index < MOUSEBUTTON_NUM; index++)
		isMouseButtonDown[index] = false;
}

void SDLIO::openMouse() {
	isMouseCapturedValue = false;
	isFirstMouseMovementSkipped = false;
	
	if (isVideoFullscreen())
		captureMouse();
	else
		releaseMouse();
}

void SDLIO::closeMouse() {
	releaseMouse();
}

void SDLIO::moveMouse(SINT32 xRelative, SINT32 yRelative) {
	if (!isFirstMouseMovementSkipped) {
		isFirstMouseMovementSkipped = true;
		return;
	}
	
	lock();

	motherboard->getMouse()->moveMouse(xRelative, yRelative);
	
	unlock();
}

void SDLIO::pushMouseButton(UINT32 button) {
	lock();
	
	UINT32 index = getMouseButtonIndex(button);
	if (index < MOUSEBUTTON_NUM) {
		if (!isMouseButtonDown[index]) {
			isMouseButtonDown[index] = true;
			motherboard->getMouse()->pushButton(index);
		}
	}
	
	unlock();
}

void SDLIO::pullMouseButton(UINT32 button) {
	lock();
	
	UINT32 index = getMouseButtonIndex(button);
	if (index < MOUSEBUTTON_NUM) {
		if (isMouseButtonDown[index]) {
			isMouseButtonDown[index] = false;
			motherboard->getMouse()->pullButton(index);
		}
	}
	
	unlock();
}

void SDLIO::clearMouseButtons() {
	lock();
	
	for (UINT32 index = 0; index < MOUSEBUTTON_NUM; index++)
		pullMouseButton(index);
	
	unlock();
}

void SDLIO::captureMouse() {
	if (!isMouseCaptured()) {
		SDL_WM_GrabInput(SDL_GRAB_ON);
		SDL_ShowCursor(false);
		isMouseCapturedValue = true;
		isFirstMouseMovementSkipped = false;
	}
}

void SDLIO::releaseMouse() {
	if (isMouseCaptured()) {
		clearMouseButtons();
		SDL_WM_GrabInput(SDL_GRAB_OFF);
		SDL_ShowCursor(true);		
		isMouseCapturedValue = false;
	}
}

bool SDLIO::isMouseCaptured() {
	return isMouseCapturedValue;
}

UINT32 SDLIO::getMouseButtonIndex(UINT32 sdlButtonIndex) {
	switch (sdlButtonIndex) {
		case SDL_BUTTON_LEFT:
			return 0;
		case SDL_BUTTON_RIGHT:
			return 1;
		case SDL_BUTTON_MIDDLE:
			return 2;
		default:
			return 3;
	}
}

void SDLIO::initJoystick() {
	joystick = NULL;
}

void SDLIO::openJoystick() {
	joystick = (SDL_NumJoysticks() > 0) ? SDL_JoystickOpen(0) : NULL;
}

void SDLIO::closeJoystick() {
	if (joystick)
		SDL_JoystickClose(joystick);
}

void SDLIO::setJoystickPosition(UINT32 axis, UINT32 value) {
	motherboard->getPaddles()->setPosition(axis, value);
}

void SDLIO::pushJoystickButton(UINT32 button) {
	lock();
	
	motherboard->getPaddles()->pushButton(button);
	
	unlock();
}

void SDLIO::pullJoystickButton(UINT32 button) {
	lock();
	
	motherboard->getPaddles()->pullButton(button);
	
	unlock();
}

void SDLIO::eventLoop() {
	bool isRunning = true;
	while (isRunning) {
		SDL_Event event;
        
		if (!SDL_WaitEvent(&event))
			break;
		
		switch (event.type) {
			case SDL_ACTIVEEVENT:
				if (event.active.gain == 0)
					releaseMouse();
				break;
			case SDL_KEYDOWN:
				pushKeyboardKey(event.key.keysym.sym,
								event.key.keysym.mod,
								event.key.keysym.unicode);
				break;
			case SDL_KEYUP:
				pullKeyboardKey(event.key.keysym.sym,
								event.key.keysym.mod,
								event.key.keysym.unicode);
				break;
			case SDL_MOUSEMOTION:
				if (isMouseCaptured())
					moveMouse(event.motion.xrel,
							  event.motion.yrel);
				break;
			case SDL_MOUSEBUTTONDOWN:
				if (isMouseCaptured())
					pushMouseButton(event.button.button);
				else
					captureMouse();
				break;
			case SDL_MOUSEBUTTONUP:
				if (isMouseCaptured())
					pullMouseButton(event.button.button);
				break;
			case SDL_JOYAXISMOTION:
				setJoystickPosition(event.jaxis.axis, 
									(event.jaxis.value + 32768) / 256);
				break;
			case SDL_JOYBUTTONDOWN:
				pushJoystickButton(event.jbutton.button);
				break;
			case SDL_JOYBUTTONUP:
				pullJoystickButton(event.jbutton.button);
				break;
			case SDL_USEREVENT:
				close();
				motherboard->getVideoRenderer()->updateVideo();
				open(motherboard, NULL);
				break;
			case SDL_QUIT:
				isRunning = false;
				break;
		}
    }
}

bool SDLIO::quit() {
	SDL_Event event;
	
	event.type = SDL_QUIT;
	return (SDL_PushEvent(&event) != -1);
}
